/*
 * Moseycode
 * Copyright (C) 2008  Tom Gibara
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package com.tomgibara.mosey.coding;

public final class URLFormEncoder {

	static int WORD_SIZE = 6;
	final static int WORD_MASK = (1 << WORD_SIZE) - 1;
	
	final static char[] CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray();
	
	private final StringBuilder builder;
	private int buffer = 0;
	private int bitCount = 0;
	
	public URLFormEncoder() {
		builder = new StringBuilder();
	}
	
	public URLFormEncoder(StringBuilder builder) {
		if (builder == null) throw new IllegalArgumentException("null builder");
		this.builder = builder;
	}
	
	public StringBuilder getBuilder() {
		return builder;
	}
	
	public void append(byte[] bytes, int offset, int length) {
		if (bitCount == -1) throw new IllegalStateException("closed");
		if (bytes == null) throw new IllegalArgumentException("null bytes");
		if (length < 0) throw new IllegalArgumentException("negative length");
		int finish = offset + length;
		if (finish >= bytes.length) throw new IllegalArgumentException("offset + length > array length");
		for (int i = offset; i < finish; i++) {
			appendByte(bytes[i]);
		}
	}

	public void append(byte[] bytes) {
		if (bitCount == -1) throw new IllegalStateException("closed");
		if (bytes == null) throw new IllegalArgumentException("null bytes");
		for (int i = 0; i < bytes.length; i++) {
			appendByte(bytes[i]);
		}
	}

	public void append(byte b) {
		if (bitCount == -1) throw new IllegalStateException("closed");
		appendByte(b);
	}
	
	public void append(short s) {
		if (bitCount == -1) throw new IllegalStateException("closed");
		buffer = (buffer << 16) | (s & 0xffff);
		bitCount += 16;
		flush();
	}
	
	public void append(int i) {
		if (bitCount == -1) throw new IllegalStateException("closed");
		if (bitCount == 0) {
			buffer = i;
			bitCount = 32;
			flush();
		} else {
			append((short) (i >> 16));
			append((short) i);
		}
	}
	
	public void close() {
		if (bitCount == -1) return; //idempotent
		if (bitCount > 0) {
			builder.append(CHARS[(buffer << WORD_SIZE - bitCount) & WORD_MASK]);
			buffer = 0;
		}
		bitCount = -1;
	}
	
	// object methods
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (!(obj instanceof URLFormEncoder)) return false;
		URLFormEncoder that = (URLFormEncoder) obj;
		return
			this.bitCount == that.bitCount &&
			this.buffer == that.buffer &&
			this.builder.equals(that.builder);
	}
	
	@Override
	public int hashCode() {
		return builder.hashCode() ^ buffer ^ (bitCount << 27);
	}
	
	@Override
	public String toString() {
		return builder.toString();
	}
	
	// private utility methods

	private void flush() {
		if (bitCount == 0) return;
		while (bitCount > WORD_SIZE) {
			builder.append(CHARS[(buffer >>> (bitCount - WORD_SIZE)) & WORD_MASK]);
			bitCount -= WORD_SIZE;
		}
	}
	
	private void appendByte(byte b) {
		buffer = (buffer << 8) | (b & 0xff);
		bitCount += 8;
		flush();
	}
	
}
